package parking.carvalho;

import java.io.Serializable;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import parking.Door;
import parking.IAlgorithm;
import parking.LogManager;
import parking.Message;
import reso.Client;
import reso.Reso;


/**
 * Class describing the Carvalho and Roucairol algorithm
 * @author Sophie and Ronnie
 * @date 9 November 2013
 */
public class Carvalho_Roucairol extends UnicastRemoteObject implements IAlgorithm, Client {
	private static final long serialVersionUID = 608944910117937034L;

	private final int ENTREE = 0;
	private final int SORTIE = 1;

	public Reso myReso;					// Mon objet distant pour la communication entre les portes
	private Door myDoor;				// Ma porte reliée à mon algorithme
	private int H;						// Estampille locale
	private int HSC;					// Estampille de demande d'entrée en SC
	private boolean R;					// Le processus est demandeur de SC
	private boolean SC;					// Le processus est en SC
	private List<Integer> X;			// Ensemble des processus dont l'avis de libération est différé 
	private List<Integer> XA; 			// Ensemble des processus desquels i attend une autorisation
	private int Nrel;					// Nombre d'avis de libération attendus
	private Set<Integer> V; 			// Voisinage de i
	private boolean init;				// Booléen pour savoir si la porte a déjà été initiée
	private int typeTemp;				// Variable temporaire pour connaitre le type de flux de voiture
	private LogManager myLogManager;	// Objet me permettant d'écrire dans un fichier log
	private Logger myLogger;			// Permet d'écrire dans le log

	private int nbPlacesLibres;			// Compteur local du nombre de places libres dans le parking

	public Carvalho_Roucairol(Door myDoor, int nbPlaces, Reso myReso) throws RemoteException {
		this.myDoor = myDoor;
		this.myReso = myReso;

		try {
			this.myDoor.setIdentity(myReso.declareClient(this));
		} catch (RemoteException e1) {
			e1.printStackTrace();
		}

		// Création du fichier de log pour la porte
		this.myLogManager = new LogManager("../logParking/Protocole_CR/" + "Log_CR" + this.myDoor.getIdentity() + ".log", Level.INFO);
		this.myLogger = this.myLogManager.getlogger();
		this.myLogger.info("Identité de la porte : " + this.myDoor.getIdentity());

		this.init = false;

		this.nbPlacesLibres = nbPlaces;
	}

	/**
	 * Initialisation des données de la porte
	 */
	private synchronized void initialisation() {
		this.myLogger.info("Initialisation des données de la porte");
		this.typeTemp = -1;
		this.H = 0;
		this.HSC = 0;
		this.R = false;
		this.SC = false;
		this.X = new ArrayList<Integer>();

		try {
			this.V = this.myReso.getClients();
			this.V.remove(this.myDoor.getIdentity());
		} catch (RemoteException e) {
			e.printStackTrace();
		}

		this.XA = new ArrayList<Integer>();
		this.XA.addAll(this.V);
		this.Nrel = 0;
	}

	/**
	 * Mise à jour du nombre de places dans le parking selon la requête reçue
	 * @param reqRcv Requête reçue
	 * @param from Porte ayant envoyé la requête
	 */
	private synchronized void updateNbSpots(RequestClock reqRcv, int from) {
		switch(reqRcv.getType()) {
		case ENTREE :
			if (this.nbPlacesLibres > 0) {
				this.nbPlacesLibres--;
				this.myLogger.info("Reçoit : " + from + " -> " + this.myDoor.getIdentity() + " | REL + UPDATE | entrée | places libres = " + this.nbPlacesLibres);
			} else {
				this.myLogger.info("Reçoit : " + from + " -> " + this.myDoor.getIdentity() + " | REL + UPDATE | refus | places libres = " + this.nbPlacesLibres);
			}
			break;

		case SORTIE :
			this.nbPlacesLibres++;
			this.myLogger.info("Reçoit : " + from + " -> " + this.myDoor.getIdentity() + " | REL + UPDATE | sortie | places libres = " + this.nbPlacesLibres);
			break;
		}
		
		if (R)
			this.acceptRel();
	}

	/**
	 * Règle 1 : Demande d'entrée en section critique
	 * @param typeSC Type de la section critique (0 pour entrée, 1 pour sortie)
	 */
	public synchronized void acceptDemandSC(int typeSC) {		
		this.R = true;
		this.HSC = this.H + 1;
		this.Nrel = this.XA.size();
		this.typeTemp = typeSC;

		RequestClock req;
		Iterator<Integer> iter = this.XA.iterator();
		while (iter.hasNext()) {
			Integer n = iter.next();
			try {
				this.myLogger.info("Envoie : " + this.myDoor.getIdentity() + " -> " + n + " | REQ | " + typeToString(typeSC) + " | H = " + this.HSC);
				req = new RequestClock(Message.REQ, this.HSC, typeSC);
				this.myReso.sendMessage(this.myDoor.getIdentity(), n, req);
			} catch (RemoteException e) {
				e.printStackTrace();
			}
		}

		while (this.Nrel != 0) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

		this.SC = true;

		/* Section Critique */
		switch (typeSC) {
		case ENTREE :
			if (this.nbPlacesLibres > 0) {
				this.nbPlacesLibres--;
				this.myLogger.info("Porte " + this.myDoor.getIdentity() + " entre en section critique | entrée | places libres = " + this.nbPlacesLibres);
			} else {
				this.myLogger.info("Porte " + this.myDoor.getIdentity() + " entre en section critique | entrée refusée | parking complet");
			}			
			break;

		case SORTIE : 
			this.nbPlacesLibres++;
			this.myLogger.info("Porte " + this.myDoor.getIdentity() + " entre en section critique | sortie | places libres = " + this.nbPlacesLibres);
			break;
		}

		this.acceptReleaseSC(typeSC);
	}

	/**
	 * Règle 2 : Accepte une requête de demande de section critique
	 * @param caller Porte demandant la section critique
	 * @param reqRcv Requête reçues
	 */
	public synchronized void acceptRequest(int caller, RequestClock reqRcv) {
		int Hr = reqRcv.getHour();
		this.myLogger.info("Reçoit : " + caller + " -> " + this.myDoor.getIdentity() + " | " + reqRcv.toString());

		this.H = Math.max(this.HSC, Hr) + 1;

		if (this.SC || (this.R && ((this.HSC < Hr) || ((this.HSC == Hr) && this.myDoor.getIdentity() < caller)))) {
			this.X.add(caller);
		} else {
			try {
				this.myLogger.info("Envoie : " + this.myDoor.getIdentity() + " -> " + caller + " | REL");
				this.myReso.sendMessage(this.myDoor.getIdentity(), caller, Message.REL);

			} catch (RemoteException e) {
				e.printStackTrace();
			}

			if (this.R && !this.SC && !this.XA.contains(caller)) {
				RequestClock req = null;
				if (this.typeTemp != -1) {
					req = new RequestClock(Message.REQ, this.HSC, this.typeTemp); 
					this.typeTemp = -1;
				}

				try {
					this.myLogger.info("Envoie : " + this.myDoor.getIdentity() + " -> " + caller + " | " + req.toString());
					if (req != null) {
						this.myReso.sendMessage(this.myDoor.getIdentity(), caller, req);
					}					
				} catch (RemoteException e) {
					e.printStackTrace();
				}
				this.Nrel++;
			}

			this.XA.add(caller);
		}


	}

	/**
	 * Règle 3 : Accepte une requête de libération
	 */
	public synchronized void acceptRel() {		
		this.Nrel--;
		this.notify();
	}

	/**
	 * Règle 4 : Fonction de fin de relachement de section critique
	 * @param typeSC Type de la section critique (0 pour entrée, 1 pour sortie)
	 */
	public synchronized void acceptReleaseSC(int typeSC) {
		this.R = false;
		this.SC = false;
		this.XA.clear();
		this.XA.addAll(this.X);

		RequestClock rel = new RequestClock(Message.REL, this.HSC, typeSC); 
		Iterator<Integer> iter = this.V.iterator();
		while (iter.hasNext()) { 
			Integer n = iter.next();
			try {
				this.myLogger.info("Envoie : " + this.myDoor.getIdentity() + " -> " + n + " | " + rel.toString());
				this.myReso.sendMessage(this.myDoor.getIdentity(), n, rel);
			} catch (RemoteException e) {
				e.printStackTrace();
			}
		}

		this.X.clear();
	}

	@Override
	public void receiveMessage(int from, int to, Serializable msg) throws RemoteException {
		if (!init) {
			this.initialisation();
			this.init = true;			
		}

		if (msg.getClass() == RequestClock.class) {
			if (((RequestClock)msg).getMsg() == Message.REQ) {
				this.acceptRequest(from, ((RequestClock) msg));				
			} else if (((RequestClock)msg).getMsg() == Message.REL) {
				this.updateNbSpots((RequestClock) msg, from);
			}			
		} else if (msg == Message.REL) {
			this.myLogger.info("Reçoit : " + this.myDoor.getIdentity() + " | REL");
			this.acceptRel();
		}
	}

	@Override
	public void demandeEntree() {
		if (!init) {
			this.initialisation();
			this.init = true;
		}

		this.acceptDemandSC(ENTREE);
	}

	@Override
	public void demandeSortie() {
		if (!init) {
			this.initialisation();
			this.init = true;			
		}

		this.acceptDemandSC(SORTIE);
	}
	
	private String typeToString(int type) {
		switch (type) {
		case 0 :
			return "entrée";
			
		case 1 : 
			return "sortie";
		}
		
		return "error";
	}


}
